package com.javabi.common.concurrent.collect.copyonwrite;

import static java.util.Collections.emptyMap;
import static java.util.Collections.unmodifiableCollection;
import static java.util.Collections.unmodifiableSet;

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.google.common.collect.ForwardingMap;

public abstract class CopyOnWriteMap<K, V> extends ForwardingMap<K, V> {

	private transient final Lock lock = new ReentrantLock();
	private volatile Map<K, V> delegate = emptyMap();

	@Override
	protected Map<K, V> delegate() {
		return delegate;
	}

	@Override
	public Set<K> keySet() {
		Map<K, V> map = this.delegate;
		return unmodifiableSet(map.keySet());
	}

	@Override
	public Collection<V> values() {
		Map<K, V> map = this.delegate;
		return unmodifiableCollection(map.values());
	}

	@Override
	public Set<Entry<K, V>> entrySet() {
		Map<K, V> map = this.delegate;
		return unmodifiableSet(map.entrySet());
	}

	@Override
	public void clear() {
		final Lock lock = this.lock;
		lock.lock();
		try {
			this.delegate = emptyMap();
		} finally {
			lock.unlock();
		}
	}

	@Override
	public V put(K key, V value) {
		final Lock lock = this.lock;
		lock.lock();
		try {
			Map<K, V> newMap = newMap(this.delegate);
			V oldValue = newMap.put(key, value);
			this.delegate = newMap;
			return oldValue;
		} finally {
			lock.unlock();
		}
	}

	@Override
	public V remove(Object key) {
		final Lock lock = this.lock;
		lock.lock();
		try {
			Map<K, V> newMap = newMap(this.delegate);
			V oldValue = newMap.remove(key);
			this.delegate = newMap;
			return oldValue;
		} finally {
			lock.unlock();
		}
	}

	@Override
	public void putAll(Map<? extends K, ? extends V> putMap) {
		final Lock lock = this.lock;
		lock.lock();
		try {
			Map<K, V> newMap = newMap(this.delegate);
			newMap.putAll(putMap);
			this.delegate = newMap;
		} finally {
			lock.unlock();
		}
	}

	protected abstract Map<K, V> newMap(Map<K, V> putMap);
}
